from django.db import models
from django.contrib.auth.models import User


class Movie(models.Model):
  # Used for weighting
  RECENT_SCREENINGS = 5

  name = models.CharField(max_length=255, primary_key=True) 
  date_added = models.DateField()
  imdb_url = models.URLField(blank=True)
  rotten_url = models.URLField(blank=True)
  metacritic_url = models.URLField(blank=True)
  voters = models.ManyToManyField(User, related_name='voted_movies', blank=True)
  downvoters = models.ManyToManyField(User, related_name='downvoted_movies', blank=True)
  tally = models.FloatField(default=0)

  rotten_score = models.IntegerField(null=True, blank=True)
  rotten_score_last_updated = models.DateField(null=True, blank=True)
  metacritic_score = models.IntegerField(null=True, blank=True)
  metacritic_score_last_updated = models.DateField(null=True, blank=True)
  review_average_score = models.FloatField(null=True, blank=True)

  class Admin:
    search_fields = ['name']

  def __str__(self):
    return self.name

  @classmethod
  def get_weight(cls, user):
    """Determine weighted value for a user's vote.

    The formula is:
    
    If there has been no screenings, always return 1.

    attended = number of screenings attended
    missed = number of screenings missed
    attended_recent = number of past RECENT_SCREENINGS screenings that were attended
    missed_recent = number of past RECENT_SCREENINGS screenings that were missed

    all_time_score = max(-0.5, min(0.5, 0.05 * (attended - missed)))
    recent_score = 0.1 * (attended_recent - missed_recent)
    lock = attended >= 1 ? 0.5 : 0
    vote value = max(0, min(1, lock + all_time_score + recent_score))
    """
    total_screenings = Screening.objects.count()
    if not total_screenings:
      # If no screenings, treat everyone's vote value as 1
      return 1

    # Figure out account inception date
    # Find out earliest screening they've attended
    # If earliest screening date is less than account inception date, use that
    inception_date = user.date_joined.date()
    attended = user.attended_screenings.count()
    if attended:
      oldest_screen_date = user.attended_screenings.order_by('date')[0].date
      if oldest_screen_date < inception_date:
        inception_date = oldest_screen_date

    screenings_inception = Screening.objects.filter(date__gte=inception_date)
    missed = screenings_inception.count() - attended
    attended_recent = 0
    missed_recent = 0
    for screening in screenings_inception.order_by('-date')[:cls.RECENT_SCREENINGS]:
      if screening.attendees.filter(username=user.username).count():
        attended_recent += 1
      else:
        missed_recent += 1

    # restrict all_time_score : -0.5 <= all_time_score <= 0.5
    all_time_score = max(-0.5, min(0.5, 0.05 * (attended - missed)))
    recent_score = 0.1 * (attended_recent - missed_recent)
    lock = 0
    if attended >= 1:
      lock = 0.5
    # restrict weight : 0 <= weight <= 1
    return max(0, min(1, lock + all_time_score + recent_score))

  def update_tally(self, weights=None):
    """weights can be a dictionary of usernames to weights."""
    tally = 0.0
    for user in self.voters.all():
      if weights:
        tally += weights[user.username]
      else:
        tally += self.get_weight(user)
    for user in self.downvoters.all():
      if weights:
        tally -= weights[user.username]
      else:
        tally -= self.get_weight(user)
    tally -= self.screening_set.count()
    if self.tally != tally:
      self.tally = tally
      self.save()

  @classmethod
  def update_all_tallies(cls):
    # Build a dictionary of weights for caching
    weights = {}
    for user in User.objects.all():
      weights[user.username] = cls.get_weight(user)
    for movie in cls.objects.all():
      movie.update_tally(weights=weights)


class Screening(models.Model):
  movie = models.ForeignKey(Movie, edit_inline=models.STACKED, num_in_admin=1)
  date = models.DateField(core=True)
  attendees = models.ManyToManyField(User, related_name='attended_screenings', blank=True)

  class Admin:
    pass

  def __str__(self):
    return self.movie.name
